package org.vacoor.xqq.core.http;

import org.apache.http.*;
import org.apache.http.client.CookieStore;
import org.apache.http.client.config.CookieSpecs;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.client.methods.HttpUriRequest;
import org.apache.http.client.methods.RequestBuilder;
import org.apache.http.concurrent.FutureCallback;
import org.apache.http.config.ConnectionConfig;
import org.apache.http.config.RegistryBuilder;
import org.apache.http.cookie.Cookie;
import org.apache.http.entity.ContentType;
import org.apache.http.impl.client.BasicCookieStore;
import org.apache.http.impl.client.DefaultConnectionKeepAliveStrategy;
import org.apache.http.impl.client.LaxRedirectStrategy;
import org.apache.http.impl.nio.client.CloseableHttpAsyncClient;
import org.apache.http.impl.nio.client.HttpAsyncClients;
import org.apache.http.impl.nio.conn.PoolingNHttpClientConnectionManager;
import org.apache.http.impl.nio.reactor.DefaultConnectingIOReactor;
import org.apache.http.impl.nio.reactor.IOReactorConfig;
import org.apache.http.nio.ContentDecoder;
import org.apache.http.nio.IOControl;
import org.apache.http.nio.client.methods.HttpAsyncMethods;
import org.apache.http.nio.conn.PlainIOSessionFactory;
import org.apache.http.nio.conn.SchemeIOSessionFactory;
import org.apache.http.nio.conn.ssl.SSLIOSessionFactory;
import org.apache.http.nio.entity.ContentBufferEntity;
import org.apache.http.nio.entity.HttpAsyncContentProducer;
import org.apache.http.nio.protocol.AbstractAsyncResponseConsumer;
import org.apache.http.nio.protocol.BasicAsyncRequestProducer;
import org.apache.http.nio.protocol.HttpAsyncRequestProducer;
import org.apache.http.nio.reactor.IOReactorException;
import org.apache.http.nio.util.HeapByteBufferAllocator;
import org.apache.http.nio.util.SimpleInputBuffer;
import org.apache.http.protocol.BasicHttpContext;
import org.apache.http.protocol.HttpContext;
import org.apache.http.util.Asserts;
import org.apache.http.util.EntityUtils;
import org.vacoor.xqq.core.exception.WebQQException;

import javax.net.ssl.KeyManager;
import javax.net.ssl.SSLContext;
import javax.net.ssl.TrustManager;
import javax.net.ssl.X509TrustManager;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.Charset;
import java.security.KeyManagementException;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;

/**
 * User: vacoor
 */
public class HttpRequestor {
    public static final String DEFAULT_USER_AGENT = Request.Header.HEADER_USER_AGENT_FF;

    private CloseableHttpAsyncClient client;
    private CookieStore cookieStore;
    private HttpContext context;

    public HttpRequestor(HttpContext context) {
        this.context = context;
    }

    private static HttpRequestor DEFAULT_REQUESTOR = new HttpRequestor(new BasicHttpContext());

    static {
        try {
            DEFAULT_REQUESTOR.init();
        } catch (IOReactorException e) {
            e.printStackTrace();
        }
    }

    public CookieStore getCookiStore() {
        return this.cookieStore;
    }

    public String getCookieValue(String name) {
        for (Cookie c : cookieStore.getCookies()) {
            if (c.getName().equals(name)) {
                return c.getValue();
            }
        }
        return "";
    }

    public static HttpRequestor getInstance() {
        return DEFAULT_REQUESTOR;
    }

    public void init() throws IOReactorException {
        /*-
//    client.getParams().setParameter(ClientPNames.ALLOW_CIRCULAR_REDIRECTS, true);
        DefaultHttpClient httpClient = new DefaultHttpClient();
        httpClient.setRedirectStrategy(new LaxRedirectStrategy()); // 从4.1开始支持Redirect 爽

        // HTTPS 配置
//        Scheme https = new Scheme("https", 443, SSLSocketFactory.getSocketFactory());
        SSLContext sslContext = SSLContext.getInstance("SSL");
        // 类似12306 会使用证书, 这里初始化为不是用密钥库, 信任任何证书
        sslContext.init(new KeyManager[]{}, new TrustManager[]{new TrustAnyTrustManager()}, new SecureRandom());
        Scheme https = new Scheme("https", 443, new SSLSocketFactory(sslContext));
        httpClient.getConnectionManager().getSchemeRegistry().register(https);

        HttpParams httpParams = httpClient.getParams();

        HttpClientParams.setCookiePolicy(httpParams, CookiePolicy.BROWSER_COMPATIBILITY); // Cookie 策略

        HttpConnectionParams.setConnectionTimeout(httpParams, 1000); // 连接超时
        HttpConnectionParams.setSoTimeout(httpParams, 4000); // 请求超时
        HttpConnectionParams.setTcpNoDelay(httpParams, true);
        HttpConnectionParams.setSocketBufferSize(httpParams, 4096);

        HttpProtocolParams.setVersion(httpParams, HttpVersion.HTTP_1_1);
//        HttpProtocolParams.setContentCharset(httpParams, "UTF-8");
//        HttpProtocolParams.setUserAgent();
        HttpProtocolParams.setUserAgent(httpParams, SimpleRequest.USER_AGENT_IE6);

        BasicClientCookie2 cookie = new BasicClientCookie2("JSESSIONID", "6DA3E8D2E572CEBCE2C09E5244B89603");
        cookie.setDomain("dynamic.12306.cn");
        cookie.setPath("/otsweb");
//        httpClient.getCookieStore().addCookie(cookie);
        Response send = httpClient.send(httpHost, httpGet);

        for(Cookie c : httpClient.getCookieStore().getCookies()) {
            System.out.println(c);
        }
        System.out.println(EntityUtils.toString(send.getEntity(), "UTF-8"));
        */

        // HTTPS
        SSLContext sslContext = null;
        try {
            sslContext = SSLContext.getInstance("SSL");
            // 类似12306 会使用证书, 这里初始化为不是用密钥库, 信任任何证书
            sslContext.init(new KeyManager[]{}, new TrustManager[]{new TrustAnyTrustManager()}, new SecureRandom());
        } catch (NoSuchAlgorithmException e) {
        } catch (KeyManagementException e) {
        }

        // 连接配置
        ConnectionConfig connConf = ConnectionConfig.custom()
                .setBufferSize(4096)
//                .setCharset(Charset.forName("UTF-8"))
                .build();

        // 请求配置
        RequestConfig reqConf = RequestConfig.custom()
                .setCookieSpec(CookieSpecs.BROWSER_COMPATIBILITY) // Cookie 为浏览器兼容模式
                .setConnectTimeout(5 * 1000)
                .setConnectionRequestTimeout(5 * 1000)  // 请求连接的超时时间
                .setSocketTimeout(5 * 1000)         // 从发起到开始建立 socket 连接的超时时间
                .setRedirectsEnabled(true)  // 启用重定向, 配合 client.setRedirectStrategy 使用
//                .setStaleConnectionCheckEnabled(true)
//                .setExpectContinueEnabled(false)
                .setRelativeRedirectsAllowed(true)
                .setMaxRedirects(10)
                .setCircularRedirectsAllowed(true)  // 允许循环重定向, QQ需要重复请求头像
                .build();

        cookieStore = new BasicCookieStore();
//        BasicClientCookie cookie = new BasicClientCookie("test", "http-client");
//        cookie.setDomain("baidu.com");
//        cookie.setPath("/");
//        cookieStore.addCookie(cookie);

        // 使用池化的连接
        IOReactorConfig reactorConfig = IOReactorConfig.custom()
//                .setConnectTimeout(100 * 1000)
                .setSoTimeout(70 * 1000)       // TODO Socket 超时时间, 避免长连接当网络断开过长时 WebQQ在连接上去服务器一直保持连接但不响应,死这
                .build();
        PoolingNHttpClientConnectionManager poolingCM = new PoolingNHttpClientConnectionManager(
//                new DefaultConnectingIOReactor(IOReactorConfig.DEFAULT),
                new DefaultConnectingIOReactor(reactorConfig),
                RegistryBuilder.<SchemeIOSessionFactory>create()
                        .register("http", PlainIOSessionFactory.INSTANCE)
                        .register("https", new SSLIOSessionFactory(sslContext))
                        .build()
        );
        poolingCM.setDefaultConnectionConfig(connConf);
        poolingCM.setDefaultMaxPerRoute(100);

        // 初始化客户端
        client = HttpAsyncClients.custom()
//                .setSSLContext(sslContext)
                .setDefaultConnectionConfig(connConf)
                .setDefaultRequestConfig(reqConf)
                .setDefaultCookieStore(cookieStore)
                .setConnectionManager(poolingCM)
//                .setRedirectStrategy(DefaultRedirectStrategy.INSTANCE) // 默认只有 GET HEAD 重定向
                .setRedirectStrategy(new LaxRedirectStrategy())  // LAX 为 GET HEAD POST,不要使用LaxRedirectStrategy.INSTANCE, 原因看源码
                .setKeepAliveStrategy(new DefaultConnectionKeepAliveStrategy() {
                    @Override
                    public long getKeepAliveDuration(HttpResponse response, HttpContext context) {
                        long timeout = super.getKeepAliveDuration(response, context);
                        return -1 == timeout ? 20 * 1000 : timeout;     // 如果不能从响应信息中获取 timeout, 则最大为 20s
                    }
                })
                .setUserAgent(DEFAULT_USER_AGENT)
                .build();
//        new IdleConnectionMonitorThread(poolingCM).start();

        client.start();
    }

    public void destory() {
        if (client != null) {
            try {
                client.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

    public Future<Response> send(Request request, RequestCallback callback) {
        RequestBuilder builder = null;
        switch (request.getMethod()) {
            case POST:
                builder = RequestBuilder.post();
                break;
            case GET:
            default:
                builder = RequestBuilder.get();
        }
        builder.setUri(request.getURL());

        for (Request.Parameter p : request.getAllParameters()) {
            builder.addParameter(p.getName(), p.getValue().toString());
        }

        for (Request.Header h : request.getAllHeaders()) {
            builder.addHeader(h.getName(), h.getValue());
        }

        if (callback != null) {
            return execute(builder.build(), new ConvertFutureCallback(callback));
        }
        return execute(builder.build(), null);
    }

    private Future<Response> execute(HttpUriRequest request, FutureCallback<Response> callback) {
//        RequestBuilder.copy(request).setConfig()
//        HttpAsyncRequestProducer req = HttpAsyncMethods.create(URIUtils.extractHost(request.getURI()), request);
        HttpAsyncRequestProducer req = HttpAsyncMethods.create(request);
        return client.execute(req, new ResponseConsumer(), this.context, callback);
    }

    private class ConvertFutureCallback implements FutureCallback<Response> {
        private RequestCallback callback;

        private ConvertFutureCallback(RequestCallback callback) {
            this.callback = callback;
        }

        @Override
        public void completed(Response result) {
            if (HttpStatus.SC_OK == result.getStateCode()) {
                try {
                    callback.onSuccess(result);
                } catch (IOException e) {
                    e.printStackTrace();
                }
            } else {
                callback.onFailure(result);
            }
        }

        @Override
        public void failed(Exception ex) {
            callback.onException(ex);
        }

        @Override
        public void cancelled() {
            callback.onCancel();
        }
    }

    /**
     * 请求生产器
     */
    private class RequestProducer extends BasicAsyncRequestProducer {

        protected RequestProducer(HttpHost target, HttpEntityEnclosingRequest request, HttpAsyncContentProducer producer) {
            super(target, request, producer);
        }
    }

    /**
     * 响应处理器
     * 定制了下结果
     * <p/>
     * BasicAsyncResponseConsumer 直接拷贝过来把 buildResult 改了,
     * warnning: 该类会将所有内容全部加载到内存
     */

    private class ResponseConsumer extends AbstractAsyncResponseConsumer<Response> {
        private volatile HttpResponse response;
        private volatile SimpleInputBuffer buf;

        public ResponseConsumer() {
            super();
        }

        @Override
        protected void onResponseReceived(final HttpResponse response) throws IOException {
            this.response = response;
        }

        @Override
        protected void onEntityEnclosed(
                final HttpEntity entity, final ContentType contentType) throws IOException {
            long len = entity.getContentLength();
            if (len > Integer.MAX_VALUE) {
                throw new ContentTooLongException("Entity Content is too long: " + len);
            }
            if (len < 0) {
                len = 4096;
            }
            this.buf = new SimpleInputBuffer((int) len, new HeapByteBufferAllocator());
            this.response.setEntity(new ContentBufferEntity(entity, this.buf));
        }

        @Override
        protected void onContentReceived(
                final ContentDecoder decoder, final IOControl ioctrl) throws IOException {
            Asserts.notNull(this.buf, "Content buffer");
            this.buf.consumeContent(decoder);
        }

        @Override
        protected Response buildResult(final HttpContext context) {
//            http.setAttribute();
            return new ConvertResponse(response);
        }

        @Override
        protected void releaseResources() {
            this.response = null;
            this.buf = null;
        }
    }

    // 将 HttpResponse 转换为 定制的 Response
    private class ConvertResponse implements Response {
        public final Charset DEFAULT_CHARSET = Charset.forName("UTF-8");

        private final HttpResponse response;
        private final Content content;

        public ConvertResponse(HttpResponse response) {
            this.response = response;
            content = new Content() {
                @Override
                public byte[] asBytes() throws WebQQException {
                    try {
                        return EntityUtils.toByteArray(ConvertResponse.this.response.getEntity());
                    } catch (IOException e) {
                        throw new WebQQException(e);
                    }
                }

                @Override
                public String asString() throws WebQQException {
                    return asString(DEFAULT_CHARSET);
                }

                @Override
                public String asString(Charset charset) throws WebQQException {
                    try {
                        return EntityUtils.toString(ConvertResponse.this.response.getEntity(), charset);
                    } catch (IOException e) {
                        throw new WebQQException(e);
                    }
                }

                @Override
                public InputStream asStream() throws WebQQException {
                    try {
                        return ConvertResponse.this.response.getEntity().getContent();
                    } catch (IOException e) {
                        throw new WebQQException(e);
                    }
                }
            };
        }

        @Override
        public int getStateCode() {
            return response.getStatusLine().getStatusCode();
        }

        @Override
        public Content getContent() {
            return this.content;
        }
    }

    /**
     * 信任任何证书的信任库管理器
     */
    private class TrustAnyTrustManager implements X509TrustManager {

        public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException {
        }

        public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException {
        }

        public X509Certificate[] getAcceptedIssuers() {
            return new X509Certificate[]{};
        }
    }

    private class IdleConnectionMonitorThread extends Thread {
        private final PoolingNHttpClientConnectionManager manager;
        private volatile boolean shutdown;

        public IdleConnectionMonitorThread(PoolingNHttpClientConnectionManager manager) {
            this.manager = manager;
        }

        @Override
        public void run() {
            try {
                while (!shutdown) {
                    synchronized (this) {
                        wait(5 * 1000);
                        manager.closeExpiredConnections();
                        manager.closeIdleConnections(5, TimeUnit.SECONDS);
                    }
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

        public void shutdown() {
            shutdown = true;
            synchronized (this) {
                notifyAll();
            }
        }
    }
}
